package com.hmall.search.service.impl;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.hmall.common.domain.PageDTO;
import com.hmall.search.domain.dto.FilterDTO;
import com.hmall.search.domain.po.ItemDoc;
import com.hmall.search.domain.query.ItemPageQuery;
import com.hmall.search.service.SearchService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.core.CountRequest;
import org.elasticsearch.common.lucene.search.function.CombineFunction;
import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Slf4j
public class SearchServiceImpl implements SearchService {
    private final RestHighLevelClient client;

    /**
     * 分页查询商品列表
     * @param query
     * @return
     * @throws IOException
     */
    public PageDTO<ItemDoc> queryItemList(ItemPageQuery query) throws IOException {
        SearchRequest request = new SearchRequest("items");
        // 1.构建bool查询条件
        BoolQueryBuilder bool = QueryBuilders.boolQuery();
        // key
        if(StrUtil.isNotBlank(query.getKey()))
            bool.must(QueryBuilders.matchQuery("name", query.getKey()));
        else
            bool.must(QueryBuilders.matchAllQuery());
        // category
        if(StrUtil.isNotBlank(query.getCategory()))
            bool.filter(QueryBuilders.matchQuery("category", query.getCategory()));
        // brand
        if(StrUtil.isNotBlank(query.getBrand()))
            bool.filter(QueryBuilders.termQuery("brand", query.getBrand()));
        // price
        if(query.getMinPrice() != null || query.getMaxPrice() != null) {
            RangeQueryBuilder range = QueryBuilders.rangeQuery("price");
            if(query.getMinPrice() != null)
                range.gte(query.getMinPrice());
            if(query.getMaxPrice() != null)
                range.lte(query.getMaxPrice());
            bool.filter(range);
        }
        // status
        // bool.filter(QueryBuilders.termQuery("status", 1)); 商品的status=1，商品信息才会插入到es中时，故这里不需要该字段

        // 2.构造算分函数
        FunctionScoreQueryBuilder.FilterFunctionBuilder[] functions = new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
                new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                    QueryBuilders.termQuery("isAD", true),
                    ScoreFunctionBuilders.weightFactorFunction(100))
        };
        FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(bool, functions)
                .scoreMode(FunctionScoreQuery.ScoreMode.SUM)
                .boostMode(CombineFunction.SUM);
        // 3.构建source
        request.source()
                .query(functionScoreQuery)                      // 3.1.查询
                .from(query.from()).size(query.getPageSize())  // 3.2分页
                .sort(SortBuilders.scoreSort().order(SortOrder.DESC))   // 3.3按照分数排序
                .sort(query.getSortBy().isBlank() ? "updateTime" : query.getSortBy(),  // 3.3排序
                query.getIsAsc() ? SortOrder.ASC : SortOrder.DESC);

        // 4.发起请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        // 5.处理结果
        SearchHits searchHits = response.getHits();
        SearchHit[] hits = searchHits.getHits();
        List<ItemDoc> itemDocList = new ArrayList<>();
        for (SearchHit hit : hits) {
            String source = hit.getSourceAsString();
            ItemDoc itemDoc = JSONUtil.toBean(source, ItemDoc.class);
            itemDocList.add(itemDoc);
        }

        // 6.获取总条数
        CountRequest countRequest = new CountRequest("items");
        countRequest.query(bool);
        long total = client.count(countRequest, RequestOptions.DEFAULT).getCount();

        // 7.构造结果
        return new PageDTO<>(total, (total + query.getPageSize() - 1) / query.getPageSize(), itemDocList);
    }

    /**
     * 查询商品过滤字段
     * @param query
     * @return
     */
    public FilterDTO queryItemFilters(ItemPageQuery query) {
        SearchRequest request = new SearchRequest("items");
        // 查询条件
        if(StrUtil.isNotBlank(query.getKey()))
            request.source().query(QueryBuilders.matchQuery("name", query.getKey()));
        else
            request.source().query(QueryBuilders.matchAllQuery());
        // 不查询文档，只做聚合
        request.source().size(0);

        // 聚合字段
        String[] aggFields = {"brand", "category"};
        // 聚合
        for (String field : aggFields) {
            request.source().aggregation(AggregationBuilders.terms(field + "Agg").field(field));
        }

        // 发起请求
        try {
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            return buildFilterDTO(response, aggFields);
        } catch (IOException e) {
            log.error("查询商品过滤字段异常", e);
            return null;
        }
    }

    private static FilterDTO buildFilterDTO(SearchResponse response, String... fields) {
        if(response == null) return null;
        FilterDTO filterDTO = new FilterDTO();
        Aggregations aggregations = response.getAggregations();
        for (String field : fields) {
            String aggName = field + "Agg";
            if(aggregations.get(aggName) == null) continue;
            Terms terms = aggregations.get(aggName);
            List<String> values = terms.getBuckets()
                    .stream()
                    .map(Terms.Bucket::getKeyAsString)
                    .collect(Collectors.toList());
            filterDTO.put(field, values);
        }
        return filterDTO;
    }
}

